package com.idemon.task;

import lombok.extern.slf4j.Slf4j;
import org.quartz.CronExpression;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;

/**
 * 定时任务核心类
 * 实现SchedulingConfigurer接口，以便可以定制定时任务（就不使用注解定时任务了@Scheduled(fixedRateString = "${rate}")）
 * 实现DisposableBean接口，以便服务停止时释放相应资源
 *
 * @author idemon
 * @since 2020-10-27 10:24
 */
@Slf4j
@Component
public class TaskConfigurer implements SchedulingConfigurer, DisposableBean {

    @Resource
    private ApplicationContext applicationContext;
    @Resource
    private TaskScheduler taskScheduler;
    @Resource
    private TaskConfigService taskConfigService;
    @Resource
    private TaskDateService taskDateService;

    /**
     * 用于保存所有自定义Task类
     * key: Task实现类注入容器的名称，这个要与TaskName枚举中的name值保持一致（例如 "airbusTask"）
     * value: com.idemon.task.Task的实现类对象 (com.idemon.task.AirbusTask)
     */
    private static Map<String, Task> TASK_MAP = new ConcurrentHashMap<>();
    /**
     * 保存目前所有已启动的com.idemon.task.Task的实现类的配置，重点就是Task类型和cron表达式
     * key: TaskConfigPo.id
     * value:   TaskConfigPo
     */
    private static Map<Long, TaskConfigPo> CRON_MAP = new ConcurrentHashMap<>();
    /**
     * 保存所有已开启定时任务：ScheduledFuture
     * key: TaskConfigPo.id
     * value: ScheduledFuture
     */
    private static Map<Long, ScheduledFuture> FUTURE_MAP = new ConcurrentHashMap<>();

    /**
     * 实现SchedulingConfigurer接口，以便可以定制定时任务
     * （就不使用注解定时任务了@Scheduled(fixedRateString = "${rate}")）
     * @param scheduledTaskRegistrar
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        // 配置TaskScheduler线程池
        scheduledTaskRegistrar.setTaskScheduler(taskScheduler);
        // 获取所有com.idemon.task.Task的实现类对应的注入对象
        TASK_MAP = applicationContext.getBeansOfType(Task.class);
        if (CollectionUtils.isEmpty(TASK_MAP)) {
            log.info("===>Task class is NULL");
            return;
        }
        // 启动任务装配
        refresh();
    }

    /**
     * 装配定时任务
     * 这里之所以仍然额外配置@Scheduled(fixedRateString = "${rate}")，
     * 主要是为了无需重启服务即可动态修改每个Task的cron，从而实现Task修改触发策略热加载
     */
    @Scheduled(fixedRateString = "${rate}")
    public void refresh() {
        // 从数据库查询所有Task对应的配置
        Map<Long, TaskConfigPo> stringTaskConfigPoMap = taskConfigService.selectList();
        // 用于保存即将要关闭的任务id列表，一般是指TaskConfigPo被删除了或者未启用（即：TaskConfigPo.id）
        Set<Long> cancels = new HashSet<>();
        // 用于保存可能需要重启的任务id列表，一般是由于修改了cron表达式而需要触发重启（即：TaskConfigPo.id）
        Set<Long> restarts = new HashSet<>();
        // 用于保存需要新启动的任务id列表，一般指首次创建的TaskConfigPo（即：TaskConfigPo.id）
        Set<Long> starts = new HashSet<>();
        //[原先已启动的Task列表] - [从数据库中查出的最新Task列表] = [需要关闭的Task id列表]
        if(!CollectionUtils.isEmpty(CRON_MAP)){
            cancels.addAll(CRON_MAP.keySet());
        }
        if(!CollectionUtils.isEmpty(stringTaskConfigPoMap)){
            cancels.removeAll(stringTaskConfigPoMap.keySet());
        }
        //[从数据库中查出的最新Task列表] - [原先已启动的Task列表] = [需要新启动的Task id列表]
        if(!CollectionUtils.isEmpty(stringTaskConfigPoMap)){
            starts.addAll(stringTaskConfigPoMap.keySet());
        }
        if(!CollectionUtils.isEmpty(CRON_MAP)){
            starts.removeAll(CRON_MAP.keySet());
        }
        // [原先已启动的Task列表] 和 [从数据库中查出的最新Task列表] 的交集即： [可能需要新启动的Task id列表]
        if(!CollectionUtils.isEmpty(CRON_MAP)){
            restarts.addAll(CRON_MAP.keySet());
        }
        if(!CollectionUtils.isEmpty(stringTaskConfigPoMap)){
            restarts.retainAll(stringTaskConfigPoMap.keySet());
        }
        // 将[需要关闭的Task]一一关闭
        if (!CollectionUtils.isEmpty(cancels)) {
            for (Long key : cancels) {
                cancel(key);
            }
        }
        // 将[需要新启动的Task]一一启动
        if (!CollectionUtils.isEmpty(starts)) {
            for (Long key : starts) {
                cancel(key);
                start(stringTaskConfigPoMap.get(key));
            }
        }
        // 将[需要重启的Task]一一重启
        if (!CollectionUtils.isEmpty(restarts)) {
            for (Long key : restarts) {
                TaskConfigPo taskConfigPo = stringTaskConfigPoMap.get(key);
                if (taskConfigPo == null) {
                    continue;
                }
                // 如果最新的cron为空，则直接关闭task
                String cron = taskConfigPo.getCron();
                if (StringUtils.isEmpty(cron)) {
                    cancel(key);
                    continue;
                }
                // 如果最新的cron与原先的cron一致，则不做任何操作，否则，重启task
                if (CRON_MAP.get(key) != null && cron.equals(CRON_MAP.get(key).getCron())) {
                    continue;
                } else {
                    cancel(key);
                    start(stringTaskConfigPoMap.get(key));
                }
            }
        }
    }

    /**
     * 关闭指定TaskConfigPo.id的Task
     * @param key
     */
    private void cancel(Long key) {
        if (CRON_MAP != null || CRON_MAP.containsKey(key)) {
            CRON_MAP.remove(key);
        }
        if (FUTURE_MAP == null || !FUTURE_MAP.containsKey(key)) {
            return;
        }
        // 关闭ScheduledFuture
        ScheduledFuture scheduledFuture = FUTURE_MAP.get(key);
        if (scheduledFuture != null) {
            scheduledFuture.cancel(true);
        }
        // 从已开启task列表中剔除
        FUTURE_MAP.remove(key);
        log.info("===>Task [TASK_CONFIG_ID:{}] is cancel", key);
    }

    /**
     * 启动一个新Task
     * @param taskConfigPo
     */
    private void start(TaskConfigPo taskConfigPo) {
        if(taskConfigPo == null){
            return;
        }

        if (taskConfigPo.getName() == null) {
            throw new RuntimeException(String.format("===>Can't find name[TASK_CONFIG_ID:%s] in the enums TaskName",
                    taskConfigPo.getId()));
        }

        if (TASK_MAP == null || !TASK_MAP.containsKey(taskConfigPo.getName().getName())) {
            return;
        }

        Trigger trigger = null;
        if (taskConfigPo.getType() != null && (taskConfigPo.getType() == TaskType.ASSIGN_DATE_TASK)) {
            // 如果Task类型为TaskType.ASSIGN_DATE_TASK，则使用AssignDateTrigger触发器
            trigger = new AssignDateTrigger(taskDateService,taskConfigPo.getId());
        } else {
            // 如果Task类型为TaskType.NORMAL_TASK，则使用CronTrigger触发器，即直接使用数据库中指定的cron表达式触发
            String cron = taskConfigPo.getCron();
            // 判断cron表达式是否合法
            if (!CronExpression.isValidExpression(cron)) {
                throw new RuntimeException(String.format("The cron of [TASK_CONFIG_ID:%s] is invalid : %s", taskConfigPo.getId(), cron));
            }
            trigger = new CronTrigger(cron);
        }
        // 通过spring容器注入的Task名称获取出对应的com.idemon.task.Task实现类对象
        Task task = TASK_MAP.get(taskConfigPo.getName().getName());
        // 这个就是最关键的地方，通过TaskScheduler线程池，并指定一个Trigger触发器，来启动一个新Task
        ScheduledFuture<?> schedule = taskScheduler.schedule(() -> {
            task.task(taskConfigPo);
        }, trigger);
        // 更新最新已启动任务列表
        FUTURE_MAP.put(taskConfigPo.getId(), schedule);
        // 更新已启动TaskConfigPo任务列表
        CRON_MAP.put(taskConfigPo.getId(), taskConfigPo);
        log.info("===>Task [TASK_CONFIG_ID:{}] is start", taskConfigPo.getId());
    }

    /**
     * 实现DisposableBean接口，以便服务停止时释放相应资源
     */
    @Override
    public void destroy() {
        if (FUTURE_MAP == null) {
            return;
        }
        for (ScheduledFuture future : FUTURE_MAP.values()) {
            if (future != null) {
                future.cancel(true);
            }
        }
        FUTURE_MAP.clear();
    }

}